package org.maxgroundstation.groundstation.comm.service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedQueue;

import javax.swing.JFrame;

import org.maxgroundstation.groundstation.GroundStation;
import org.maxgroundstation.groundstation.GroundStationEvent;
import org.maxgroundstation.groundstation.comm.service.alive.ImAliveService;
import org.maxgroundstation.groundstation.comm.service.echo.EchoService;
import org.maxgroundstation.groundstation.comm.service.list.ListService;
import org.maxgroundstation.groundstation.comm.service.video.VideoService;

/**
 * Handles all service communication.
 * @author Scott Davis
 * 
 * Copyright (C) 2011  Scott Davis
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
public class ServiceCommunication {

	// Default client ID.
	public static final String DEFAULT_CLIENT_ID = new String(new byte[8]);
	
	// Static members for UDP and TCP communication.
	public static final int TCP = 0;
	public static final int UDP = 1;
	
	// Data members.
	private ServiceCommunicationUDP commUDP;
	private ServiceCommunicationTCP commTCP;
	private GroundStation station;
	private String clientID;
	private String ipAddress;
	private boolean useTCP;
	private ConcurrentLinkedQueue<Service> services;
	private ConcurrentLinkedQueue<Service> subscribedServices;
	private List<JFrame> serviceFrames;
	
	/**
	 * Constructor.
	 * @param station the ground station.
	 */
	public ServiceCommunication(GroundStation station) {
		this.station = station;
		clientID = DEFAULT_CLIENT_ID;
		ipAddress = "";
		commUDP = new ServiceCommunicationUDP(this);
		commTCP = new ServiceCommunicationTCP(this);
		useTCP = false;
		serviceFrames = new ArrayList<JFrame>();
		
		// Set default services.
		services = new ConcurrentLinkedQueue<Service>();
		subscribedServices = new ConcurrentLinkedQueue<Service>();
		addDefaultServices();
	}
	
	/**
	 * Gets the client ID.
	 * @return the client ID.
	 */
	String getClientID() {
		return clientID;
	}
	
	/**
	 * Sets the client ID.
	 * @param clientID the client ID.
	 */
	void setClientID(String clientID) {
		if (!this.clientID.equals(clientID)) {
			String oldClientID = this.clientID;
			this.clientID = clientID;
			if (!oldClientID.equals(DEFAULT_CLIENT_ID)) {
				// Resubscribe to services.
				System.out.println("New client ID");
				System.out.println("Resubscribing to services");
				subscribeServices();
			}
		}
	}
	
    /**
     * Gets the current IP address.
     * @return IP address as string.
     */
    public String getIPAddress() {
        return ipAddress;
    }
    
    /**
     * Sets the IP address.
     * @param ipAddress the new IP address.
     */
    public void setIPAddress(String ipAddress) {
        this.ipAddress = ipAddress;
    }
    
    /**
     * Gets the current communication type.
     * @return communication type (UDP or TCP).
     */
    public int getCommType() {
    	if (useTCP) return TCP;
    	else return UDP;
    }
    
    /**
     * Sets the communication type.
     * @param commType the communication type (UDP or TCP).
     */
    public void setCommType(int commType) {
    	if (commType == TCP) useTCP = true;
    	else if (commType == UDP) useTCP = false;
    	else throw new IllegalArgumentException("comm type: " + commType + " invalid.");
    }
    
    /**
     * Gets the ground station.
     * @return ground station.
     */
    public GroundStation getGroundStation() {
    	return station;
    }
    
    /**
     * Gets the UDP port.
     * @return UDP port.
     */
    public int getUDPPort() {
    	return commUDP.getPort();
    }
    
    /**
     * Sets the UDP port.
     * @param port the UDP port.
     */
    public void setUDPPort(int port) {
    	commUDP.setPort(port);
    }
    
    /**
     * Gets the TCP port.
     * @return TCP port.
     */
    public int getTCPPort() {
    	return commTCP.getPort();
    }
    
    /**
     * Sets the TCP port.
     * @param port the TCP port.
     */
    public void setTCPPort(int port) {
    	commTCP.setPort(port);
    }
    
    /**
     * Connect the service communication.
     * @throws IOException if error connecting the service communication.
     */
    public void connect() throws IOException {
    	
    	// Make communication connections.
    	commUDP.connect();
    	commTCP.connect();
    	
    	try {
    		Thread.sleep(500L);
    	}
    	catch (InterruptedException e) {};
    	
    	// Send post to request service list.
    	System.out.println("Requesting list of services");
    	String dataString = "list";
    	String listServiceID = ServiceUtil.getServiceID("__list");
    	try {
    		sendServicePost(listServiceID, dataString);
    	}
    	catch (Exception e) {
    		e.printStackTrace(System.err);
    	}
    }
    
    /**
     * Disconnect the service communication.
     * @throws IOException if error disconnecting service communication.
     */
    public void disconnect() throws IOException {
    	
    	// Close down all services.
    	closeDownServices();
    	
    	// Disconnect communication.
    	commUDP.disconnect();
    	commTCP.disconnect();
    	
    	// Set the client ID to default value.
    	clientID = DEFAULT_CLIENT_ID;
    }
    
    /**
     * Sends a service post with a message to the robot.
     * @param serviceID the service ID string.
     * @param message the message string.
     * @throws Exception if error sending service post.
     */
    public void sendServicePost(String serviceID, String message) throws Exception {
    	if (message == null) throw new IllegalArgumentException("message cannot be null");
    	sendServicePost(serviceID, message.getBytes());
    }
    
    /**
     * Sends a service post with data to the robot.
     * @param serviceID the service ID string.
     * @param data the data bytes.
     * @throws Exception if error sending service post.
     */
    public void sendServicePost(String serviceID, byte[] data) throws Exception {
    	if (data == null) throw new IllegalArgumentException("data cannot be null");
    	int commType = UDP;
    	if (useTCP) commType = TCP;
    	sendServicePost(serviceID, data, commType);
    }
    
    /**
     * Sends a service post with data to the robot.
     * @param serviceID the service ID string.
     * @param data the data bytes.
     * @param commType the communication type (UDP or TCP).
     * @throws Exception if error sending service post.
     */
    public void sendServicePost(String serviceID, byte[] data, int commType) throws Exception {
    	if (data == null) throw new IllegalArgumentException("data cannot be null");
    	if (commType == UDP) commUDP.sendServicePost(serviceID, data);
    	else if (commType == TCP) commTCP.sendServicePost(serviceID, data);
    	else throw new IllegalArgumentException("comm type: " + commType + " not valid");
    }
    
    /**
     * Handle a data response.
     * @param dataFrame the response data frame.
     */
    void handleResponseData(ServiceDataFrame dataFrame) {
		
		String serviceID = dataFrame.getServiceID().trim();
		
		// Determine service to handle response.
		Service handlingService = null;
		Iterator<Service> i = services.iterator();
		while (i.hasNext()) {
			Service service = i.next();
			if (service.getID().trim().equals(serviceID)) handlingService = service;
		}
		
		if (handlingService != null) handlingService.handleResponse(dataFrame);
		else System.err.println("No service found to handle response for ID: " + serviceID);
	}
    
    /**
     * Sets the list of all available services.
     * @param newServices list of new services.
     */
    public void setServices(List<Service> newServices) {
        // Close down old services.
        closeDownServices();
        
    	services = new ConcurrentLinkedQueue<Service>(newServices);
    	
    	// Initialize services.
    	initializeServices();
    	
    	// Fire ground station event.
    	station.fireGroundStationEvent(GroundStationEvent.SERVICES_INITIALIZED, services);
    }
    
    /**
     * Gets a list of all available services.
     * @return list of services.
     */
    public List<Service> getServices() {
    	return new ArrayList<Service>(services);
    }
    
    /**
     * Checks if a service that has the given service ID exists.
     * @param serviceID the service ID.
     * @return true if service exists.
     */
    public boolean hasService(String serviceID) {
    	boolean result = false;
    	
    	Iterator<Service> i = services.iterator();
    	while (i.hasNext()) {
    		Service service = i.next();
    		if (service.getID().equals(serviceID)) result = true;
    	}
    	
    	return result;
    }
    
    /**
     * Gets a service the matches the given service ID.
     * @param serviceID the service ID.
     * @return service or null if none found.
     */
    public Service getService(String serviceID) {
    	Service result = null;
    	
    	Iterator<Service> i = services.iterator();
    	while (i.hasNext()) {
    		Service service = i.next();
    		if (service.getID().equals(serviceID)) result = service;
    	}
    	
    	return result;
    }
    
    /**
     * Gets a service that matches the given name.
     * @param serviceName the service name.
     * @return service or null if none found.
     */
    public Service getServiceByName(String serviceName) {
        Service result = null;
        
        Iterator<Service> i = services.iterator();
        while (i.hasNext()) {
            Service service = i.next();
            if (service.getName().equals(serviceName)) result = service;
        }
        
        return result;
    }
    
    /**
     * Perform any work required to initialize services.
     */
    private void initializeServices() {
    	
    	// Initialize each service.
    	Iterator<Service> k = services.iterator();
    	while (k.hasNext()) {
    		k.next().initialize();
    	}
    	
    	// Subscribe to any subscribable services.
    	subscribeServices();
    }
    
    /**
     * Closes down all services.
     */
    private void closeDownServices() {
    	
    	// Unsubscribe to subscribable services.
    	unsubscribeServices();
    	
    	// Close service frames.
    	Iterator<JFrame> i = serviceFrames.iterator();
    	while (i.hasNext()) {
    		JFrame serviceFrame = i.next();
    		serviceFrame.setVisible(false);
    		serviceFrame.dispose();
    	}
    	
    	// Close down each service.
    	Iterator<Service> k = services.iterator();
    	while (k.hasNext()) {
    		k.next().closeDown();
    	}
    	
    	// Clear services and set default services.
    	serviceFrames.clear();
    	services.clear();
    	subscribedServices.clear();
    	addDefaultServices();
    	
    	// Fire ground station event.
        station.fireGroundStationEvent(GroundStationEvent.SERVICES_CLOSED);
    }
    
    /**
     * Subscribe to all subscribable services.
     */
    public void subscribeServices() {
        boolean videoServiceSubscribed = false;
    	Iterator<Service> j = services.iterator();
    	while (j.hasNext()) {
    		Service service = j.next();
    		if (service.isSubscribable()) {
    		    
    		    if (service.getType().equals(VideoService.TYPE)) {
    		        // Check if video service is already subscribed.
    		        // If not, subscribe to video service.
    		        if (!videoServiceSubscribed) {
    		            subscribeService(service);
    		            videoServiceSubscribed = true;
    		        }
    		    }
    		    else {
    		        subscribeService(service);
    		    }
    		}
    	}
    }
    
    /**
     * Subscribe to a service.
     * @param service the service.
     */
    public void subscribeService(Service service) {
        if (service.isSubscribable()) {
            System.out.println("Subscribing to service: " + service.getID());
            String dataString = "start=" + service.getID();
            String listServiceID = ServiceUtil.getServiceID("__list");
            try {
                sendServicePost(listServiceID, dataString);
                
                if (!isServiceSubscribed(service)) {
                    subscribedServices.add(service);
                }
            }
            catch (Exception e) {
                e.printStackTrace(System.err);
            }
        }
        else {
            System.err.println("Attempting to subscribe to service " + service.getName() + 
                    " not it is not subscribable.");
        }
    }
    
    /**
     * Unsubscribe to all subscribed services.
     */
    public void unsubscribeServices() {
    	Iterator<Service> j = services.iterator();
    	while (j.hasNext()) {
    		Service service = j.next();
    		if (service.isSubscribable() && isServiceSubscribed(service)) {
    			unsubscribeService(service);
    		}
    	}
    }
    
    /**
     * Unsubscribe to a service.
     * @param service the service.
     */
    public void unsubscribeService(Service service) {
        if (service.isSubscribable()) {
            System.out.println("Unsubscribing to service: " + service.getID());
            String dataString = "stop=" + service.getID();
            String listServiceID = ServiceUtil.getServiceID("__list");
            try {
                sendServicePost(listServiceID, dataString);
                
                if (isServiceSubscribed(service)) {
                    subscribedServices.remove(service);
                }
            }
            catch (Exception e) {
                e.printStackTrace(System.err);
            }
        }
        else {
            System.err.println("Attempting to unsubscribe to service " + service.getName() + 
                    " not it is not subscribable.");
        }
    }
    
    /**
     * Checks if a services is currently subscribed to.
     * @param service the service.
     * @return true if service is subscribed to.
     */
    public boolean isServiceSubscribed(Service service) {
        return subscribedServices.contains(service);
    }
    
    /**
     * Adds the default services to the services list.
     */
    private void addDefaultServices() {
    	//services.add(new ImAliveService(this));
    	services.add(new EchoService(this));
		services.add(new ListService(this));
    }
}